<!doctype html>
<html lang="zh-CN">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width,initial-scale=1">
  <title>虚拟乐器模拟器</title>
  <script src="https://cdn.tailwindcss.com"></script>
  <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.7.2/css/all.min.css" rel="stylesheet">
  <script>
    tailwind.config = {
      theme: {
        extend: {
          colors: {
            primary: "#3B82F6",
            secondary: "#EC4899",
            accent: "#10B981",
            dark: "#1E293B",
            light: "#F8FAFC"
          },
          fontFamily: {
            sans: ["Inter", "system-ui", "sans-serif"]
          }
        }
      }
    }
  </script>
  <style type="text/tailwindcss">@layer utilities {
      .content-auto {
        content-visibility: auto;
      }
      .piano-key {
        transition: all 0.1s ease;
      }
      .piano-key-active {
        transform: translateY(5px);
      }
      .guitar-string {
        transition: all 0.1s ease;
      }
      .guitar-string-active {
        transform: scaleY(0.95) translateX(2px);
      }
      .drum-pad {
        transition: all 0.15s ease;
      }
      .drum-pad-active {
        transform: scale(0.95);
        box-shadow: 0 0 15px rgba(59, 130, 246, 0.7);
      }
      .glow {
        animation: glow 1.5s ease-in-out infinite alternate;
      }
      @keyframes glow {
        from {
          box-shadow: 0 0 5px rgba(59, 130, 246, 0.3);
        }
        to {
          box-shadow: 0 0 20px rgba(59, 130, 246, 0.7);
        }
      }
      .fade-in {
        animation: fadeIn 0.5s ease-in;
      }
      @keyframes fadeIn {
        from { opacity: 0; }
        to { opacity: 1; }
      }
      .slide-up {
        animation: slideUp 0.5s ease-out;
      }
      @keyframes slideUp {
        from { transform: translateY(30px); opacity: 0; }
        to { transform: translateY(0); opacity: 1; }
      }
    }</style>
</head>

<body class="bg-gradient-to-br from-dark to-gray-800 text-light min-h-screen">
  <header class="w-full py-4 px-6 bg-dark/80 backdrop-blur-md border-b border-primary/20 sticky top-0 z-50">
    <div class="container mx-auto flex justify-between items-center">
      <div class="flex items-center gap-3"><i class="fa-solid fa-music text-primary text-2xl"></i>
        <h1
          class="text-[clamp(1.5rem,3vw,2.5rem)] font-bold bg-clip-text text-transparent bg-gradient-to-r from-primary to-secondary">
          虚拟乐器模拟器</h1>
      </div>
      <nav>
        <ul class="flex gap-6 text-gray-300">
          <li><a href="#" class="hover:text-primary transition-colors duration-300 flex items-center gap-1"><i
                class="fa-solid fa-home"></i> 首页</a></li>
          <li><a href="#" class="hover:text-primary transition-colors duration-300 flex items-center gap-1"><i
                class="fa-solid fa-book"></i> 教程</a></li>
          <li><a href="#" class="hover:text-primary transition-colors duration-300 flex items-center gap-1"><i
                class="fa-solid fa-user"></i> 关于</a></li>
        </ul>
      </nav>
    </div>
  </header>
  <main class="container mx-auto px-4 py-8">
    <section class="mb-12 fade-in">
      <div class="flex flex-wrap justify-center gap-4 mb-8"><button id="piano-btn"
          class="instrument-btn active bg-primary hover:bg-primary/90 text-white px-6 py-3 rounded-lg shadow-lg transition-all duration-300 flex items-center gap-2"><i
            class="fa-solid fa-music"></i> 钢琴</button> <button id="guitar-btn"
          class="instrument-btn bg-gray-700 hover:bg-gray-600 text-white px-6 py-3 rounded-lg shadow-lg transition-all duration-300 flex items-center gap-2"><i
            class="fa-solid fa-guitar"></i> 吉他</button> <button id="drums-btn"
          class="instrument-btn bg-gray-700 hover:bg-gray-600 text-white px-6 py-3 rounded-lg shadow-lg transition-all duration-300 flex items-center gap-2"><i
            class="fa-solid fa-drum"></i> 架子鼓</button></div>
    </section>
    <section id="piano" class="instrument-section slide-up">
      <div class="relative w-full max-w-5xl mx-auto overflow-hidden rounded-xl shadow-2xl">
        <div class="bg-gradient-to-b from-gray-900 to-gray-800 p-4 border border-gray-700 rounded-t-xl">
          <h2 class="text-2xl font-bold text-center mb-2">钢琴模拟器</h2>
          <p class="text-center text-gray-400 text-sm">点击琴键或使用键盘按键 A - L 演奏</p>
        </div>
        <div class="piano-container relative bg-gray-900 p-4 pb-8">
          <div id="piano-keys" class="flex justify-center"></div>
        </div>
      </div>
    </section>
    <section id="guitar" class="instrument-section hidden slide-up">
      <div
        class="w-full max-w-4xl mx-auto bg-gradient-to-b from-gray-900 to-gray-800 rounded-xl shadow-2xl overflow-hidden">
        <div class="p-4 border border-gray-700 rounded-t-xl">
          <h2 class="text-2xl font-bold text-center mb-2">吉他模拟器</h2>
          <p class="text-center text-gray-400 text-sm">点击琴弦或使用键盘按键 Z - M 演奏</p>
        </div>
        <div class="guitar-container p-6">
          <div class="relative">
            <div id="guitar-neck"
              class="bg-gradient-to-r from-gray-700 to-gray-600 h-24 rounded-l-lg relative overflow-hidden">
              <div class="absolute inset-y-0 left-1/4 w-px bg-gray-400/50"></div>
              <div class="absolute inset-y-0 left-3/8 w-px bg-gray-400/50"></div>
              <div class="absolute inset-y-0 left-1/2 w-px bg-gray-400/50"></div>
              <div class="absolute inset-y-0 left-5/8 w-px bg-gray-400/50"></div>
              <div class="absolute inset-y-0 left-3/4 w-px bg-gray-400/50"></div>
              <div id="guitar-strings" class="absolute inset-0 flex flex-col justify-around px-4"></div>
            </div>
            <div class="bg-gradient-to-r from-gray-800 to-gray-700 h-16 w-16 rounded-tr-lg"></div>
          </div>
        </div>
      </div>
    </section>
    <section id="drums" class="instrument-section hidden slide-up">
      <div
        class="w-full max-w-4xl mx-auto bg-gradient-to-b from-gray-900 to-gray-800 rounded-xl shadow-2xl overflow-hidden">
        <div class="p-4 border border-gray-700 rounded-t-xl">
          <h2 class="text-2xl font-bold text-center mb-2">架子鼓模拟器</h2>
          <p class="text-center text-gray-400 text-sm">点击鼓垫或使用键盘按键 1 - 9 演奏</p>
        </div>
        <div class="drums-container p-6">
          <div id="drum-pads" class="grid grid-cols-3 gap-4"></div>
        </div>
      </div>
    </section>
    <section
      class="mt-12 bg-gray-900/80 backdrop-blur-md p-6 rounded-xl shadow-xl border border-gray-700 max-w-4xl mx-auto fade-in">
      <div class="flex flex-wrap justify-between items-center gap-4">
        <div class="volume-control flex items-center gap-3"><i class="fa-solid fa-volume-high text-primary"></i> <input
            type="range" id="volume" min="0" max="1" step="0.05" value="0.7"
            class="w-32 h-2 bg-gray-700 rounded-lg appearance-none cursor-pointer accent-primary"></div>
        <div class="recorder flex items-center gap-4"><button id="record-btn"
            class="bg-red-600 hover:bg-red-700 text-white px-4 py-2 rounded-full flex items-center gap-2 transition-all duration-300"><i
              class="fa-solid fa-circle"></i> 录制</button> <button id="play-btn"
            class="bg-gray-700 hover:bg-gray-600 text-white px-4 py-2 rounded-full flex items-center gap-2 transition-all duration-300"
            disabled="disabled"><i class="fa-solid fa-play"></i> 播放</button> <button id="save-btn"
            class="bg-gray-700 hover:bg-gray-600 text-white px-4 py-2 rounded-full flex items-center gap-2 transition-all duration-300"
            disabled="disabled"><i class="fa-solid fa-download"></i> 保存</button></div>
        <div class="theme-toggle"><button id="theme-btn"
            class="bg-gray-700 hover:bg-gray-600 text-white p-2 rounded-full transition-all duration-300"><i
              class="fa-solid fa-moon"></i></button></div>
      </div>
    </section>
  </main>
  <footer class="mt-16 bg-dark/80 backdrop-blur-md border-t border-primary/20 py-6 px-4">
    <div class="container mx-auto text-center text-gray-400">
      <p>© 2025 虚拟乐器模拟器 | 使用 HTML, CSS 和 JavaScript 构建</p>
      <div class="flex justify-center gap-4 mt-4"><a href="#"
          class="hover:text-primary transition-colors duration-300"><i class="fa-brands fa-github"></i></a> <a href="#"
          class="hover:text-primary transition-colors duration-300"><i class="fa-brands fa-twitter"></i></a> <a href="#"
          class="hover:text-primary transition-colors duration-300"><i class="fa-brands fa-instagram"></i></a></div>
    </div>
  </footer>
  <script>
    const AudioContext = window.AudioContext || window.webkitAudioContext,
      audioContext = new AudioContext;
    let masterGainNode = audioContext.createGain();
    masterGainNode.gain.value = .7, masterGainNode.connect(audioContext.destination);
    const instruments = {
      piano: {
        keys: [{
          note: "C",
          key: "A",
          frequency: 261.63
        }, {
          note: "D",
          key: "S",
          frequency: 293.66
        }, {
          note: "E",
          key: "D",
          frequency: 329.63
        }, {
          note: "F",
          key: "F",
          frequency: 349.23
        }, {
          note: "G",
          key: "G",
          frequency: 392
        }, {
          note: "A",
          key: "H",
          frequency: 440
        }, {
          note: "B",
          key: "J",
          frequency: 493.88
        }, {
          note: "C5",
          key: "K",
          frequency: 523.25
        }, {
          note: "D5",
          key: "L",
          frequency: 587.33
        }],
        createElement(e, t) {
          const n = e.includes("#");
          return `\n            <div class="piano-key ${n?"absolute bg-black h-64 w-10 -z-10 mx-[-25px] rounded-b-lg shadow-lg hover:bg-gray-800":"bg-white h-96 w-20 border border-gray-300 rounded-b-lg shadow-lg hover:bg-gray-100"} flex flex-col justify-end items-center p-4 cursor-pointer" \n                data-note="${e}" \n                data-key="${t}">\n              <span class="text-xl font-bold ${n?"text-white":"text-gray-800"}">${e}</span>\n              <span class="text-xs mt-2 ${n?"text-gray-300":"text-gray-500"}">${t}</span>\n            </div>\n          `
        }
      },
      guitar: {
        strings: [{
          note: "E4",
          key: "Z",
          frequency: 329.63
        }, {
          note: "B3",
          key: "X",
          frequency: 246.94
        }, {
          note: "G3",
          key: "C",
          frequency: 196
        }, {
          note: "D3",
          key: "V",
          frequency: 146.83
        }, {
          note: "A2",
          key: "B",
          frequency: 110
        }, {
          note: "E2",
          key: "N",
          frequency: 82.41
        }, {
          note: "E5",
          key: "M",
          frequency: 659.25
        }],
        createElement: (e, t, n) =>
          `\n            <div class="guitar-string h-[${10-n}px] bg-gradient-to-r from-gray-300 to-${`rgb(${255-30*n}, ${255-20*n}, ${200-10*n})`} rounded-full cursor-pointer shadow-md hover:shadow-lg" \n                data-note="${e}" \n                data-key="${t}">\n              <div class="absolute left-4 bg-gray-800/80 text-white text-xs px-2 py-1 rounded">${e}</div>\n              <div class="absolute right-4 bg-gray-800/80 text-white text-xs px-2 py-1 rounded">${t}</div>\n            </div>\n          `
      },
      drums: {
        pads: [{
          sound: "底鼓",
          key: "1",
          color: "#3B82F6",
          frequency: 50
        }, {
          sound: "军鼓",
          key: "2",
          color: "#EC4899",
          frequency: 200
        }, {
          sound: "踩镲",
          key: "3",
          color: "#10B981",
          frequency: 1e3
        }, {
          sound: "通鼓1",
          key: "4",
          color: "#8B5CF6",
          frequency: 150
        }, {
          sound: "通鼓2",
          key: "5",
          color: "#F59E0B",
          frequency: 120
        }, {
          sound: "通鼓3",
          key: "6",
          color: "#EF4444",
          frequency: 100
        }, {
          sound: "叮叮镲",
          key: "7",
          color: "#06B6D4",
          frequency: 2e3
        }, {
          sound: "节奏镲",
          key: "8",
          color: "#14B8A6",
          frequency: 1500
        }, {
          sound: "牛铃",
          key: "9",
          color: "#A855F7",
          frequency: 3e3
        }],
        createElement: (e, t, n) =>
          `\n            <div class="drum-pad bg-${n} hover:bg-${n}/90 rounded-xl shadow-lg p-6 flex flex-col items-center justify-center cursor-pointer" \n                data-sound="${e}" \n                data-key="${t}">\n              <i class="fa-solid fa-drum text-4xl mb-2"></i>\n              <span class="text-xl font-bold">${e}</span>\n              <span class="text-xs mt-2 bg-black/30 px-2 py-1 rounded-full">${t}</span>\n            </div>\n          `
      }
    };
    let isRecording = !1,
      recording = [],
      startTime = 0;
    const pianoBtn = document.getElementById("piano-btn"),
      guitarBtn = document.getElementById("guitar-btn"),
      drumsBtn = document.getElementById("drums-btn"),
      pianoSection = document.getElementById("piano"),
      guitarSection = document.getElementById("guitar"),
      drumsSection = document.getElementById("drums"),
      pianoKeys = document.getElementById("piano-keys"),
      guitarStrings = document.getElementById("guitar-strings"),
      drumPads = document.getElementById("drum-pads"),
      volumeControl = document.getElementById("volume"),
      recordBtn = document.getElementById("record-btn"),
      playBtn = document.getElementById("play-btn"),
      saveBtn = document.getElementById("save-btn"),
      themeBtn = document.getElementById("theme-btn");

    function initInstruments() {
      instruments.piano.keys.forEach((e => {
        pianoKeys.innerHTML += instruments.piano.createElement(e.note, e.key)
      })), instruments.guitar.strings.forEach(((e, t) => {
        guitarStrings.innerHTML += instruments.guitar.createElement(e.note, e.key, t)
      })), instruments.drums.pads.forEach((e => {
        drumPads.innerHTML += instruments.drums.createElement(e.sound, e.key, e.color)
      })), addEventListeners()
    }

    function addEventListeners() {
      pianoBtn.addEventListener("click", (() => switchInstrument("piano"))), guitarBtn.addEventListener("click", (() =>
          switchInstrument("guitar"))), drumsBtn.addEventListener("click", (() => switchInstrument("drums"))),
        volumeControl.addEventListener("input", (e => {
          masterGainNode.gain.value = parseFloat(e.target.value)
        })), document.querySelectorAll(".piano-key").forEach((e => {
          e.addEventListener("mousedown", (() => handleMouseDown(e, "piano"))), e.addEventListener("mouseup", (() =>
            handleMouseUp(e, "piano"))), e.addEventListener("mouseleave", (() => handleMouseUp(e, "piano")))
        })), document.querySelectorAll(".guitar-string").forEach((e => {
          e.addEventListener("mousedown", (() => handleMouseDown(e, "guitar"))), e.addEventListener("mouseup", (
          () => handleMouseUp(e, "guitar"))), e.addEventListener("mouseleave", (() => handleMouseUp(e, "guitar")))
        })), document.querySelectorAll(".drum-pad").forEach((e => {
          e.addEventListener("mousedown", (() => handleMouseDown(e, "drums"))), e.addEventListener("mouseup", (() =>
            handleMouseUp(e, "drums"))), e.addEventListener("mouseleave", (() => handleMouseUp(e, "drums")))
        })), document.addEventListener("keydown", (e => {
          if (e.repeat) return;
          const t = document.querySelector(".instrument-section:not(.hidden)").id;
          let n;
          "piano" === t ? n = document.querySelector(`.piano-key[data-key="${e.key.toUpperCase()}"]`) : "guitar" ===
            t ? n = document.querySelector(`.guitar-string[data-key="${e.key.toUpperCase()}"]`) : "drums" === t && (
              n = document.querySelector(`.drum-pad[data-key="${e.key}"]`)), n && (e.preventDefault(),
              handleMouseDown(n, t))
        })), document.addEventListener("keyup", (e => {
          const t = document.querySelector(".instrument-section:not(.hidden)").id;
          let n;
          "piano" === t ? n = document.querySelector(`.piano-key[data-key="${e.key.toUpperCase()}"]`) : "guitar" ===
            t ? n = document.querySelector(`.guitar-string[data-key="${e.key.toUpperCase()}"]`) : "drums" === t && (
              n = document.querySelector(`.drum-pad[data-key="${e.key}"]`)), n && (e.preventDefault(),
              handleMouseUp(n, t))
        })), recordBtn.addEventListener("click", toggleRecording), playBtn.addEventListener("click", playRecording),
        saveBtn.addEventListener("click", saveRecording), themeBtn.addEventListener("click", toggleTheme)
    }

    function handleMouseDown(e, t) {
      const n = "piano" === t || "guitar" === t ? e.dataset.note : e.dataset.sound;
      activeOscillators[n] || playNote(n, t)
    }

    function handleMouseUp(e, t) {
      stopNote("piano" === t || "guitar" === t ? e.dataset.note : e.dataset.sound, t)
    }

    function switchInstrument(e) {
      document.querySelectorAll(".instrument-btn").forEach((e => {
        e.classList.remove("active", "bg-primary"), e.classList.add("bg-gray-700")
      })), document.getElementById(`${e}-btn`).classList.add("active", "bg-primary"), document.getElementById(
        `${e}-btn`).classList.remove("bg-gray-700"), document.querySelectorAll(".instrument-section").forEach((e => {
        e.classList.add("hidden")
      })), document.getElementById(e).classList.remove("hidden")
    }
    const activeOscillators = {};

    function playNote(e, t) {
      let n;
      if ("suspended" === audioContext.state && audioContext.resume(), "piano" === t || "guitar" === t) {
        n = instruments[t]["piano" === t ? "keys" : "strings"].find((t => t.note === e)).frequency
      } else {
        n = instruments.drums.pads.find((t => t.sound === e)).frequency
      }
      const o = audioContext.createOscillator(),
        r = audioContext.createGain();
      o.type = "sine", o.frequency.setValueAtTime(n, audioContext.currentTime), r.gain.setValueAtTime(0, audioContext
          .currentTime), r.gain.linearRampToValueAtTime(.8, audioContext.currentTime + .05), "drums" === t && r.gain
        .exponentialRampToValueAtTime(.001, audioContext.currentTime + .3), o.connect(r), r.connect(masterGainNode), o
        .start(), activeOscillators[e] = {
          oscillator: o,
          gainNode: r
        };
      const a = "piano" === t ? document.querySelector(`.piano-key[data-note="${e}"]`) : "guitar" === t ? document
        .querySelector(`.guitar-string[data-note="${e}"]`) : document.querySelector(`.drum-pad[data-sound="${e}"]`);
      if (a && ("piano" === t ? a.classList.add("piano-key-active") : "guitar" === t ? a.classList.add(
          "guitar-string-active") : a.classList.add("drum-pad-active")), isRecording) {
        const n = audioContext.currentTime - startTime;
        recording.push({
          note: e,
          instrument: t,
          time: n
        })
      }
    }

    function stopNote(e, t) {
      if (activeOscillators[e]) {
        const {
          oscillator: n,
          gainNode: o
        } = activeOscillators[e];
        "drums" !== t ? (o.gain.cancelScheduledValues(audioContext.currentTime), o.gain.linearRampToValueAtTime(0,
            audioContext.currentTime + .1), n.stop(audioContext.currentTime + .1)) : n.stop(audioContext.currentTime),
          delete activeOscillators[e]
      }
      const n = "piano" === t ? document.querySelector(`.piano-key[data-note="${e}"]`) : "guitar" === t ? document
        .querySelector(`.guitar-string[data-note="${e}"]`) : document.querySelector(`.drum-pad[data-sound="${e}"]`);
      n && ("piano" === t ? n.classList.remove("piano-key-active") : "guitar" === t ? n.classList.remove(
        "guitar-string-active") : n.classList.remove("drum-pad-active"))
    }

    function toggleRecording() {
      isRecording ? (isRecording = !1, recordBtn.innerHTML = '<i class="fa-solid fa-circle"></i> 录制', recordBtn
        .classList.remove("bg-red-700"), recordBtn.classList.add("bg-red-600"), recording.length > 0 && (playBtn
          .disabled = !1, saveBtn.disabled = !1)) : (isRecording = !0, recording = [], startTime = audioContext
        .currentTime, recordBtn.innerHTML = '<i class="fa-solid fa-circle"></i> 录制中...', recordBtn.classList.add(
          "bg-red-700"), recordBtn.classList.remove("bg-red-600"), playBtn.disabled = !0, saveBtn.disabled = !0)
    }

    function playRecording() {
      if (0 === recording.length) return;
      audioContext.currentTime;
      recording.forEach((e => {
        setTimeout((() => {
          playNote(e.note, e.instrument), "drums" !== e.instrument && setTimeout((() => {
            stopNote(e.note, e.instrument)
          }), 300)
        }), 1e3 * e.time)
      }))
    }

    function saveRecording() {
      if (0 === recording.length) return;
      const e = JSON.stringify(recording),
        t = new Blob([e], {
          type: "application/json"
        }),
        n = URL.createObjectURL(t),
        o = document.createElement("a");
      o.href = n, o.download = "music_recording.json", document.body.appendChild(o), o.click(), document.body
        .removeChild(o), URL.revokeObjectURL(n)
    }

    function toggleTheme() {
      document.body.classList.toggle("bg-gradient-to-br"), document.body.classList.toggle("from-dark"), document.body
        .classList.toggle("to-gray-800"), document.body.classList.toggle("bg-gray-100"), document.body.classList.toggle(
          "text-gray-900");
      const e = document.body.classList.contains("from-dark");
      themeBtn.innerHTML = e ? '<i class="fa-solid fa-sun"></i>' : '<i class="fa-solid fa-moon"></i>';
      document.querySelectorAll("header, section, footer, .drum-pad, .piano-key, .guitar-string").forEach((t => {
        e ? (t.classList.remove("bg-gray-200", "bg-white", "border-gray-300"), t.classList.add("bg-gray-900",
          "bg-gray-800", "border-gray-700")) : (t.classList.remove("bg-gray-900", "bg-gray-800",
          "border-gray-700"), t.classList.add("bg-gray-200", "bg-white", "border-gray-300"))
      }))
    }
    window.addEventListener("DOMContentLoaded", (() => {
      initInstruments(), switchInstrument("piano")
    }))
  </script>
</body>

</html>